Twitter retweet agent (#1181)

* Agent to retweet all received tweet events

* Cache twitter rest client

* Update description, remove unused local

* Makes context strings more readable, style consistency

* to_h is not implemented in ruby 2.0

* In error case, include all agent_ids and event_ids

* Adds capability to favorite tweets

This restructures the Agent slightly to allow for retweeting and
favoriting. It is possible to do both at the same time.

- Renames the Agent from TwitterRetweetAgent
to TwitterActionAgent.
- Specs refactored

Jack Wilson 8 years ago
parent
commit
414743556f

+ 1 - 1
app/concerns/twitter_concern.rb

@@ -36,7 +36,7 @@ module TwitterConcern
36 36
   end
37 37
 
38 38
   def twitter
39
-    Twitter::REST::Client.new do |config|
39
+    @twitter ||= Twitter::REST::Client.new do |config|
40 40
       config.consumer_key = twitter_consumer_key
41 41
       config.consumer_secret = twitter_consumer_secret
42 42
       config.access_token = twitter_oauth_token

+ 74 - 0
app/models/agents/twitter_action_agent.rb

@@ -0,0 +1,74 @@
1
+module Agents
2
+  class TwitterActionAgent < Agent
3
+    include TwitterConcern
4
+
5
+    cannot_be_scheduled!
6
+
7
+    description <<-MD
8
+      The Twitter Action Agent is able to retweet or favorite tweets from the events it receives.
9
+
10
+      #{ twitter_dependencies_missing if dependencies_missing? }
11
+
12
+      It expects to consume events generated by twitter agents where the payload is a hash of tweet information. The existing TwitterStreamAgent is one example of a valid event producer for this Agent.
13
+
14
+      To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first.
15
+
16
+      Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent.
17
+      Set `retweet` to either true or false.
18
+      Set `favorite` to either true or false.
19
+    MD
20
+
21
+    def validate_options
22
+      unless options['expected_receive_period_in_days'].present?
23
+        errors.add(:base, "expected_receive_period_in_days is required")
24
+      end
25
+      unless retweet? || favorite?
26
+        errors.add(:base, "at least one action must be true")
27
+      end
28
+    end
29
+
30
+    def working?
31
+      last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
32
+    end
33
+
34
+    def default_options
35
+      {
36
+        'expected_receive_period_in_days' => '2',
37
+        'favorite' => 'false',
38
+        'retweet' => 'true',
39
+      }
40
+    end
41
+
42
+    def retweet?
43
+      boolify(options['retweet'])
44
+    end
45
+
46
+    def favorite?
47
+      boolify(options['favorite'])
48
+    end
49
+
50
+    def receive(incoming_events)
51
+      tweets = tweets_from_events(incoming_events)
52
+
53
+      begin
54
+        twitter.favorite(tweets) if favorite?
55
+        twitter.retweet(tweets) if retweet?
56
+      rescue Twitter::Error => e
57
+        create_event :payload => {
58
+          'success' => false,
59
+          'error' => e.message,
60
+          'tweets' => Hash[tweets.map { |t| [t.id, t.text] }],
61
+          'agent_ids' => incoming_events.map(&:agent_id),
62
+          'event_ids' => incoming_events.map(&:id)
63
+        }
64
+      end
65
+    end
66
+
67
+    def tweets_from_events(events)
68
+      events.map do |e|
69
+        Twitter::Tweet.new(id: e.payload["id"], text: e.payload["text"])
70
+      end
71
+    end
72
+  end
73
+end
74
+

+ 158 - 0
spec/models/agents/twitter_action_agent_spec.rb

@@ -0,0 +1,158 @@
1
+require 'rails_helper'
2
+
3
+describe Agents::TwitterActionAgent do
4
+  describe '#receive' do
5
+    before do
6
+      @event1 = Event.new
7
+      @event1.agent = agents(:bob_twitter_user_agent)
8
+      @event1.payload = { id: 123, text: 'So awesome.. gotta retweet' }
9
+      @event1.save!
10
+      @tweet1 = Twitter::Tweet.new(
11
+        id: @event1.payload[:id],
12
+        text: @event1.payload[:text]
13
+      )
14
+
15
+      @event2 = Event.new
16
+      @event2.agent = agents(:bob_twitter_user_agent)
17
+      @event2.payload = { id: 456, text: 'Something Justin Bieber said' }
18
+      @event2.save!
19
+      @tweet2 = Twitter::Tweet.new(
20
+        id: @event2.payload[:id],
21
+        text: @event2.payload[:text]
22
+      )
23
+    end
24
+
25
+    context 'when set up to retweet' do
26
+      before do
27
+        @agent = build_agent({
28
+          'expected_receive_period_in_days' => '2',
29
+          'favorite' => 'false',
30
+          'retweet' => 'true',
31
+        })
32
+        @agent.save!
33
+      end
34
+
35
+      context 'when the twitter client succeeds retweeting' do
36
+        it 'should retweet the tweets from the payload' do
37
+          mock(@agent.twitter).retweet([@tweet1, @tweet2])
38
+          @agent.receive([@event1, @event2])
39
+        end
40
+      end
41
+
42
+      context 'when the twitter client fails retweeting' do
43
+        it 'creates an event with tweet info and the error message' do
44
+          stub(@agent.twitter).retweet(anything) {
45
+            raise Twitter::Error.new('uh oh')
46
+          }
47
+
48
+          @agent.receive([@event1, @event2])
49
+
50
+          failure_event = @agent.events.last
51
+          expect(failure_event.payload[:error]).to eq('uh oh')
52
+          expect(failure_event.payload[:tweets]).to eq(
53
+            {
54
+              @event1.payload[:id].to_s => @event1.payload[:text],
55
+              @event2.payload[:id].to_s => @event2.payload[:text]
56
+            }
57
+          )
58
+          expect(failure_event.payload[:agent_ids]).to match_array(
59
+            [@event1.agent_id, @event2.agent_id]
60
+          )
61
+          expect(failure_event.payload[:event_ids]).to match_array(
62
+            [@event2.id, @event1.id]
63
+          )
64
+        end
65
+      end
66
+    end
67
+
68
+    context 'when set up to favorite' do
69
+      before do
70
+        @agent = build_agent(
71
+          'expected_receive_period_in_days' => '2',
72
+          'favorite' => 'true',
73
+          'retweet' => 'false',
74
+        )
75
+        @agent.save!
76
+      end
77
+
78
+      context 'when the twitter client succeeds favoriting' do
79
+        it 'should favorite the tweets from the payload' do
80
+          mock(@agent.twitter).favorite([@tweet1, @tweet2])
81
+          @agent.receive([@event1, @event2])
82
+        end
83
+      end
84
+
85
+      context 'when the twitter client fails retweeting' do
86
+        it 'creates an event with tweet info and the error message' do
87
+          stub(@agent.twitter).favorite(anything) {
88
+            raise Twitter::Error.new('uh oh')
89
+          }
90
+
91
+          @agent.receive([@event1, @event2])
92
+
93
+          failure_event = @agent.events.last
94
+          expect(failure_event.payload[:error]).to eq('uh oh')
95
+          expect(failure_event.payload[:tweets]).to eq(
96
+            {
97
+              @event1.payload[:id].to_s => @event1.payload[:text],
98
+              @event2.payload[:id].to_s => @event2.payload[:text]
99
+            }
100
+          )
101
+          expect(failure_event.payload[:agent_ids]).to match_array(
102
+            [@event1.agent_id, @event2.agent_id]
103
+          )
104
+          expect(failure_event.payload[:event_ids]).to match_array(
105
+            [@event2.id, @event1.id]
106
+          )
107
+        end
108
+      end
109
+    end
110
+  end
111
+
112
+  describe "#validate_options" do
113
+    context 'when set up to neither favorite or retweet' do
114
+      it 'is invalid' do
115
+        agent = build_agent(
116
+          'expected_receive_period_in_days' => '2',
117
+          'favorite' => 'false',
118
+          'retweet' => 'false',
119
+        )
120
+
121
+        expect(agent).not_to be_valid
122
+      end
123
+    end
124
+  end
125
+
126
+  describe '#working?' do
127
+    before do
128
+      stub.any_instance_of(Twitter::REST::Client).retweet(anything)
129
+    end
130
+
131
+    it 'checks if events have been received within the expected time period' do
132
+      agent = build_agent(
133
+        'expected_receive_period_in_days' => '2',
134
+        'favorite' => 'false',
135
+        'retweet' => 'true',
136
+      )
137
+      agent.save!
138
+
139
+      expect(agent).not_to be_working # No events received
140
+
141
+      described_class.async_receive(agent.id, [events(:bob_website_agent_event)])
142
+      expect(agent.reload).to be_working # Just received events
143
+
144
+      two_days_from_now = 2.days.from_now
145
+      stub(Time).now { two_days_from_now }
146
+      expect(agent.reload).not_to be_working # Too much time has passed
147
+    end
148
+  end
149
+
150
+  def build_agent(options)
151
+    described_class.new do |agent|
152
+      agent.name = 'twitter stuff'
153
+      agent.options = options
154
+      agent.service = services(:generic)
155
+      agent.user = users(:bob)
156
+    end
157
+  end
158
+end